Python 解包 - unpacking

Python 解包 - unpacking

参考文档:

一问告诉你什么是python解包(Unpacking)_*range(4),r_LoveMIss-Y的博客-CSDN博客

Python - 解包的各种骚操作 - 小菠萝测试笔记 - 博客园

解包(Unpacking),顾名思义,就是将容器里面的元素逐个取出来

注意,这个概念,Java 中是没有的。

Python 中解包操作,在某些场景下是自动完成的,在某些场景下需要借助操作符:* 操作符和 ** 操作符,

使用 * 和 ** 的解包的好处是能节省代码量,使得代码看起来更优雅。

注意:任何可迭代对象都支持解包,可迭代对象包括元组、字典、集合、字符串、生成器等实现了 __next__ 方法的一切对象

变量赋值过程中的自动解包

容器/序列的自动解包

注意,字典解包后,只会把字典的 key 取出来,val 则丢掉了。我们在很多地方都注意到了这个现象

# 两个变量的类型都是 str
char_a, char_b = "ab"
print(char_a, type(char_a), char_b, type(char_b))
# 两个变量是int类型
list_ele_a, list_ele_b = [1212, 1313]
print(list_ele_a, type(list_ele_a), list_ele_b, type(list_ele_b))
# 两个变量是字符串类型
tuple_ele_a, tuple_ele_b = ("bbbb", "cccc")
print(tuple_ele_a, type(tuple_ele_a), tuple_ele_b, type(tuple_ele_b))
# 元组的隐式声明
tuple_ele_a, tuple_ele_b = "bbbb", "cccc"
print(tuple_ele_a, type(tuple_ele_a), tuple_ele_b, type(tuple_ele_b))
# 变量都是int类型,且元素并不是按照 1414, 1515, 1616, 1717赋值,而是任意顺序
set_ele_a, set_ele_b, set_ele_c, set_ele_d = {1414, 1515, 1616, 1717}
print(set_ele_a, type(set_ele_a), set_ele_b, type(set_ele_b), set_ele_c, type(set_ele_c), set_ele_d, type(set_ele_d))
# 字典的自动解包,只获取了key,丢掉了val
# 变量都是int类型,且元素按照 1414, 1515, 1616, 1717赋值
dict_ele_a, dict_ele_b, dict_ele_c, dict_ele_d = {1818: "aa", 1919: "bb", 2020: "cc", 2121: "dd"}
print(dict_ele_a, type(dict_ele_a), dict_ele_b, type(dict_ele_b), dict_ele_c, type(dict_ele_c), dict_ele_d,
      type(dict_ele_d))

输出:

a <class 'str'> b <class 'str'>
1212 <class 'int'> 1313 <class 'int'>
bbbb <class 'str'> cccc <class 'str'>
bbbb <class 'str'> cccc <class 'str'>
1616 <class 'int'> 1515 <class 'int'> 1717 <class 'int'> 1414 <class 'int'>
1818 <class 'int'> 1919 <class 'int'> 2020 <class 'int'> 2121 <class 'int'>

如果可迭代对象中元素的个数大于接收变量的个数怎么办? 在其中一个变量名前加上 *,注意只能给一个变量加,给两个加会报错

最终的效果就是带 * 号的变量变成一个列表(不论等号右边是什么类型,带星号的都会变成列表),且带 * 号的变量包含的元素中的值在等号右侧的相对位置,跟变量在等号左侧的变量列表所处的相对位置相同

a, *b, c = [1, 2, 3, 4, 5]
# 1 [2, 3, 4] 5
print(a, b, c)
a, *b, c = (1, 2, 3, 4, 5)
# b依然是一个列表
# 1 [2, 3, 4] 5
print(a, b, c)
a, *b, c = {1, 2, 3, 4, 5}
# b依然是一个列表
# 1 [2, 3, 4] 5
print(a, b, c)

输出:

1 [2, 3, 4] 5
1 [2, 3, 4] 5
1 [2, 3, 4] 5

如果可迭代对象只包含一个元素,我只想要这个元素,而不是想要这个可迭代对象对象,在变量后面加一个逗号即可

# a 为 int 类型
a, = [1]
print(a, type(a))
# a 为 list 类型
a = [1]
print(a, type(a))

输出:

1 <class 'int'>
[1] <class 'list'>

多变量的赋值

多变量的赋值与交换本质上也是自动解包,因为等号右边的会被看作是元组对象

int_a, int_b, int_c = 1, 2, 3
print(int_a, type(int_a), int_b, type(int_b), int_c, type(int_c))
# 实际上等价于
int_a, int_b, int_c = (1, 2, 3)
print(int_a, type(int_a), int_b, type(int_b), int_c, type(int_c))

输出:

1 <class 'int'> 2 <class 'int'> 3 <class 'int'>
1 <class 'int'> 2 <class 'int'> 3 <class 'int'>

多变量的交换

轻松实现不需要声明第三个变量的两值交换,牛逼

int_a, int_b, int_c = int_c, int_a, int_b,
# 3 1 2
print(int_a, int_b, int_c)
int_a, int_b, int_c = int_c - int_a, int_c + int_b, int_a + int_b,
# -1 3 4
print(int_a, int_b, int_c)

输出:

3 1 2
-1 3 4

利用这个特性,我们可以非常方便地计算斐波那契数列

# 计算斐波那契数列
a = 0
b = 1
for i in range(10):
    a, b = b, a + b
# 输出 89
print(b)

输出:

89

表达式中的解包

我们在学习我们在学习各种容器的各种 API,本质上是把容器当成一个整体来操作,这种操作是无法跨容器类型的,现在有了解包操作,我们可以直接对容器中的元素进行操作,自然也就不存在容器类型的限制了。通过解包操作,我们可以轻松实现各种不同类型的容器中的元素的苹姐,这在没有解包操作的时候,都是非常繁琐的。

tuple_1 = *range(4), 4
# (0, 1, 2, 3, 4)
print(tuple_1)
# range直接解包无法用变量承接,加一个, 表示用元组承接
tuple_2 = *range(4),
print(tuple_2)
# [0, 1, 2, 3, 4]
print([*range(4), 4])
# {0, 1, 2, 3, 4}
print({*range(4), 4})
# 这个操作实际上相当于拼接了两个字典
# **对字典的解包,只能用在{}内部,好像是这样
# {'x': 1, 'y': 2, 'z': 3}
print({'x': 1, **{'y': 2, 'z': 3}})
# 例如,相当于啥都没做
dict_ele_a, dict_ele_b = {**{"a": 1, "b": 3}}
print(dict_ele_a, dict_ele_b)

# list的拼接
list_raw = ["a", "b", "c"]
list_compose = [*range(4), *list_raw]
# 输出 [0, 1, 2, 3, 'a', 'b', 'c']
print(list_compose)

tuple_raw = ("1", "2", "3")
tuple_compose = (*tuple_raw, *range(4))
# 输出 ('1', '2', '3', 0, 1, 2, 3)
print(tuple_compose)

set_raw = {"1", "2", "3"}
set_compose = {*set_raw, *range(4)}
# 输出 {0, 1, 2, 3, '1', '3', '2'}
print(set_compose)

# 两个字典的拼接
dict_raw = {'x': 1, 'y': 2, 'z': 3}
dict_compose = {"a": "bc", **dict_raw}
# 输出 {'a': 'bc', 'x': 1, 'y': 2, 'z': 3}
print(dict_compose)

输出:

(0, 1, 2, 3, 4)
(0, 1, 2, 3)
[0, 1, 2, 3, 4]
{0, 1, 2, 3, 4}
{'x': 1, 'y': 2, 'z': 3}
a b
[0, 1, 2, 3, 'a', 'b', 'c']
('1', '2', '3', 0, 1, 2, 3)
{0, 1, 2, 3, '2', '1', '3'}
{'a': 'bc', 'x': 1, 'y': 2, 'z': 3}

看起来好像没啥了不起,但是实际上,  list 类型无法与 range 对象相加,你必须先将 range() 强制转换为 list 对象才能做 + 操作,因此 * 的解包操作实际上还是很方便的

我们可以看到,** 对字典的解包,只能用在 {} 内部使用,好像是这样

以上好像看不出什么,现在才是解包的妙用,通过解包,我们可以将元组和集合中的元素合并成一个列表

tuple_2_use = ("1", "2", "3",)
set_2_use = {"a", "b", "c"}
list_result = [*tuple_2_use, *set_2_use]
# 输出 ['1', '2', '3', 'c', 'a', 'b']
print(list_result)

输出:

['1', '2', '3', 'c', 'b', 'a']

函数调用过程中的解包

需要用到前面提到的 *** 这两个符号,函数被调用的时候,使用星号 * 解包一个可迭代对象作为函数的参数,作为位置参数传递给函数。字典对象,可以使用 **,解包之后将作为关键字参数传递给函数。

使用 * 和 ** 的解包的好处是能节省代码量,使得代码看起来更优雅

这个不要跟方法声明语句中的参数的 * 搞混,参考 《函数.md》中的 参数名以*开头 小节

Python3.5,也就是 PEP 448 对解包操作做了进一步扩展, 在 3.5 之前的版本,函数调用时,一个函数中解包操作只允许一个 * 和 一个 **。从 3.5 开始,在函数调用中,可以有任意多个解包操作。

先定义两个函数:

def func_test_unpacking(a, b, c):
    print(a, b, c)


def func_test_unpacking_over(a, *b, c):
    print(a, b, c)

然后先对可迭代对象进行解包,以解包后的结果为参数调用函数

func_test_unpacking(*"abc")
func_test_unpacking(*["aaa", "bbb", "ccc"])
func_test_unpacking(*(1, 1, 1))
func_test_unpacking(*{1, "aa", "bbb"})

输出:

a b c
aaa bbb ccc
1 1 1
1 aa bbb

如果可迭代对象的个数比方法的参数多,函数也可以用 * 放到变量前面,对应上面的 func_test_unpacking_over 方法,这样,这个方法形参就会将多的参数汇总为一个元组,此时,后面的形参就必须以关键字参数的格式传参。

参考 《函数.md》中的 参数名以*开头 小节

# 输出 a ('b', 'c', 'd', 'e', 'f', 'g') &&
# a 为 a
# b 为内容为 ('b', 'c', 'd', 'e', 'f', 'g') 的元组
# c 为 &&  为关键字参数
func_test_unpacking_over(*"abcdefg", c="&&")

输出:

a ('b', 'c', 'd', 'e', 'f', 'g') &&

字典有点特殊

* 来解包字典的时候,会跟前面一样将 key 作为结果传入函数,val 丢弃掉,用 ** 来解包字典的时候,会以关键字参数的形式传入参数,其中字典的 key 就是对应参数名,val 就是参数值

注意字典不能包含比方法参数多的 key,不然会报错,能不能少得看少的那个关键字参数有没有默认值,有的话就可以省略

# 用 * 来解包字典的时候,会跟前面一样将key作为结果传入函数
# 输出 1 2 3
func_test_unpacking(*{1: "c", "2": "b", 3: "c"})
# 用 ** 来解包字典的时候,会以关键字参数的形式传入参数,其中字典的key就是对应参数名,val就是参数值
# 注意字典不能包含比方法参数多的key,不然会报错,能不能少得看少的那个关键字参数有没有默认值,有的话就可以省略
# 输出 10 11 12
func_test_unpacking(**{"a": 10, "b": 11, "c": 12})

输出:

1 2 3
10 11 12